#!/usr/bin/env python
# -*- mode: python; coding: utf-8; -*-
##--------------------------------------------------------------------------##
##
## Copyright (C) 1998-2003 Markus Oberhumer <markus@oberhumer.com>
## Copyright (C) 2003 Mt. Hood Playing Card Co.
## Copyright (C) 2005-2010 Skomoroh <skomoroh@users.sourceforge.net>
## Copyright (C) 2010-2013 Jeremy Austin-Bardo <tjaustinbardo@gmail.com>
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, either version 3 of the License.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program.  If not, see <http://www.gnu.org/licenses/>.
##
##--------------------------------------------------------------------------##
"""
Stack <- DragStack
  Implement basic drag methods for the stack. Dragging of stacks is animated
  on the canvas for some mouse types. Depends on various methods of Stack.
"""
__all__ = ['DragStack_Methods']

import logging

Logger = logging.getLogger('PySolFC.Stack.DragPile')

from ..utillib import misc
from ..settings import CONF_OPT
from ..viewlib.constants import CURSOR_DRAG, ANCHOR_NW, ANCHOR_SE
from ..viewlib.canvas import ImageItem
from ..viewlib.image import PilImage
from ..utillib.image_pil import Image as _Image


MARK = '#6ae400'

class DragPileItem(object):
    """
    Card composite for drag pile.
    """
    card_pil_image = lambda s, c: c._pil_image

    def __init__(self, cards, func):
        """
        Initialize a card for a drag pile.

        .. \__init__(cards:seq(Card ...), func) -> Null
        """
        self.position = func
        self.cards = cards
        size = misc.Size(*self.card_pil_image(cards[0]).size)
        box = zip(func(cards[0]), func(cards[-1]))
        bbox = misc.Block(l=min(box[0]), t=min(box[1]),
                         r=(max(box[0]) + size.w), b=(max(box[1]) + size.h))
        self.size = misc.Size(w=bbox.r - bbox.l, h=bbox.b - bbox.t)
        self.bbox = bbox

    def __composite(self):
        """
        __composite() -> Image
        Create card composite mask for shadow.
        """
        image = _Image.new('RGBA', self.size)
        bbox = self.bbox
        for card in self.cards:
            xcoord, ycoord = self.position(card)
            tmp = self.card_pil_image(card)
            image.paste(tmp, (xcoord - bbox.l, ycoord - bbox.t), tmp)
        return image

    def shadow(self, offset, color=(0x00, 0x00, 0x00, 0x50,)):
        """
        shadow(offset:seq(int, int)[, color:seq(int, int, int)=(0x00, 0x00,
          0x00, 0x50,)]) -> PilImage
        Create card composite shadow of **color**.
        """
        size = self.size
        mask = self.__composite()
        # create shadow
        shadow = _Image.new('RGBA', (size.w, size.h))
        shadow.paste(color, (0, 0, size.w, size.h), mask)
        xoffset, yoffset = offset
        tmp = _Image.new('RGBA', (size.w - xoffset, size.h - yoffset))
        shadow.paste(tmp, (0, 0), mask.crop((xoffset, yoffset, size.w, size.h)))
        return PilImage(image=shadow)

    def highlight(self, color='#6ae400', factor=0.3):
        """
        highlight([color:str='#6ae400', factor:float=0.3]) -> PilImage

        Create card composite highlight of **color** with blending **factor**.
        """
        highlight = self.__composite()
        tmp = _Image.blend(highlight, Image.new('RGBA', highlight.size, color),
                          factor)
        highlight = _Image.composite(tmp, highlight, highlight)
        return PilImage(image=highlight)


class DragStack_Methods(object):
    """
    Implement basic drag methods for the stack. Dragging of stacks is animated
    on the canvas for some mouse types.
    """

    __shade_cache = None

    def startDrag(self, event, sound=True):
        """
        startDrag(event:obj[, sound:bool=True]) -> None

        Begin drag Stack operation

        :cvar int lastx: initial event x-coordinate.
        :cvar int lasty: initial event y-coordinate.
        :cvar game.drag: initialize a drag pile object.

        :param object event: Canvas Event.
        :keyword boolean sound: sound fx?
        """
        Logger.info('event (%s, %s)', event.x, event.y)
        game = self.game
        drag, images = game.drag, game.images
        assert drag['stack'] is None
        i = self._locate(event)
        if i < 0 or not self.cardsPlay(self.cards[i:]):
            return
        if self.is_filled and self.items['shade_item']:
            self.items['shade_item'].hide()
        x_offset, y_offset = self.cards[i].x, self.cards[i].y
        if sound and self.game.can_play_sound:
            game._play_sound('startdrag')
        self.lastx, self.lasty = event.x, event.y
        drag['start_x'], drag['start_y'] = event.x, event.y
        drag['stack'] = self
        drag['noshade_stacks'] = [self]
        drag['cards'] = self.getDragCards(i)
        drag['index'] = i
        if CONF_OPT['mouse_type'] == 'point-n-click':
            self.__markCards(drag)
            return
        drag['shadows'] = self.createShadows(drag['cards'])
        shade, size = images.shadow_offset, images.size
        margin = self.canvas._cinfo_margin
        dx, dy = 0, 0
        if CONF_OPT['mouse_type'] == 'sticky-mouse':  # return cards under mouse
            dx = event.x - (x_offset + size.w - shade.x) - margin.x
            dy = event.y - (y_offset + size.h - shade.y) - margin.y
            dx = 0 if dx < 0 else dx
            dy = 0 if dy < 0 else dy
        for pile in drag['shadows']:
            if dx > 0 or dy > 0:
                pile.move(dx, dy)
            pile.raise_item()
        for card in drag['cards']:
            card.raise_item()
            card.move(-shade.x + dx, -shade.y + dy)
        if CONF_OPT['dragcursor']:
            self.canvas.config(cursor=CURSOR_DRAG)

    def keepDrag(self, event):
        """
        keepDrag(event:obj) -> None

        Conitune drag Stack operation

        :cvar int lastx: initial event x-coordinate.
        :cvar int lasty: initial event y-coordinate.
        :cvar boolean | Null game.drag['event']: drag event status.

        :param object event: canvas event.
        """
        drag = self.game.drag
        if not drag['cards']:
            return
        assert self is drag['stack']
        dx, dy = event.x - self.lastx, event.y - self.lasty
        if dx or dy:
            self.lastx, self.lasty = event.x, event.y
            if CONF_OPT['shade']:
                self.__updateShade()
            for s in drag['shadows']:
                s.move(dx, dy)
            for card in drag['cards']:
                card.move(dx, dy)
        drag['event'] = None

    def keepDragTimer(self):
        """
        keepDragTimer() -> None

        Drag operation timer

        :cvar object game.drag['timer']:
        """
        drag = self.game.drag
        if drag['event']:
            self.keepDrag(drag['event'])
            self.canvas.update_idletasks()

    def createShadows(self, cards, dx=0, dy=0):
        """
        createShadows(cards:obj[, dx:int=0, dy:int=0]) -> seq

        Create shadows for floating drag piles

        :param sequence cards: Card sequence.
        :keyword integer dx: x-coordinate.
        :keyword integer dy: y-coordinate.
        :return: tuple of shadow image
        """
        if not CONF_OPT['shadow'] or self.game.is_preview:
            return ()
        l = len(cards)
        if l == 0 or l > self.max_shadow_cards:
            return ()
        images = self.game.images
        size = images.size
        coord = misc.Point(size.w + dx, size.h + dy)
        index = 0 if self.CARD_XOFFSET[0] < 0 else -1
        index = 0 if self.CARD_YOFFSET[0] < 0 else index
        shade = DragPileItem(cards, func=self.getPosition)
        self.__shade_cache = shade.shadow(images.shadow_offset)
        pile = ImageItem(self.canvas, cards[index].coord._move(coord),
                         image=self.__shade_cache, anchor=ANCHOR_SE)
        self.canvas.tag_lower(pile, below_this=cards[index]._item)
        return (pile,)

    def getDragCards(self, index):
        """
        getDragCards(index:int) -> seq

        Get cards for drag pile

        :param integer index: starting pile position.
        :return: cards being moved.
        :rtype: sequence
        """
        return self.cards[index:]

    def __deleteShade(self):
        """
        Called by _stopDrag() and __updateShade() to delete drag pile shadow.

        :cvar object game.drag: unset drag pile's shadow.
        """
        drag = self.game.drag
        if drag['shade_img']:
            drag['shade_img'].delete()
        drag['shade_img'] = None
        drag['shade_stack'] = None

    def __updateShade(self):
        """
        Called by keepDrag() to update pile accept highlight below drag pile.

        :cvar object game.drag: update drag pile's shadow.
        """
        game = self.game
        drag, images = game.drag, game.images
        size = images.size
        card = drag['cards'][0]
        stacks = (game.get_closest_stack(card, drag['stack']),)
        rect1 = misc.Block(l=card.x, t=card.y,
                          r=card.x + size.w, b=card.y + size.h)
        sstack, sdiff, coord = None, 999999999, misc.Point(0, 0)
        for stack in stacks:
            if stack is None or stack in drag['noshade_stacks']:
                continue
            card = stack.TOPCARD if stack.cards else stack
            rect2 = misc.Block(l=card.x, t=card.y,
                              r=card.x + size.w, b=card.y + size.h)
            if (rect1.r <= rect2.l or rect1.b <= rect2.t or
                rect2.r <= rect1.l or rect2.b <= rect1.t):
                # Rectangles do not intersect
                continue
            if stack in drag['canshade_stacks']:
                pass
            elif stack.cardsBuild(drag['stack'], drag['cards']):
                drag['canshade_stacks'].append(stack)
            else:
                drag['noshade_stacks'].append(stack)
                continue
            diff = (rect1.l - rect2.l) ** 2 + (rect1.t - rect2.t) ** 2
            if diff < sdiff:
                sstack, sdiff = stack, diff
                coord = misc.Point(int(rect2.l), int(rect2.t))
        if sstack is drag['shade_stack']:
            return
        if sstack is None:
            self.__deleteShade()
            return
        if drag['shade_img']:
            self.__deleteShade()
        # create the shade image
        drag['shade_stack'] = sstack
        drag['shade_img'] = ImageItem(self.canvas, coord,
                                      image=images.highlight)
        drag['shade_img'].raise_item()
        drag['stack'].group.raise_item()

    def unshadeStack(self):
        """
        unshadeStack() -> None

        Called by keepDrag() to unset drag piles shade item

        :cvar object items['shade_item']: unset values.
        """
        shade_item = self.items['shade_item']
        if shade_item:
            shade_item.delete()
            shade_item = None

    def __markCards(self, drag, color=MARK, factor=0.3):
        """
        Called by startDrag() to highlight drag pile instead of drag animation
        with *point-n-click* mouse type.

        :cvar object game.drag: set shadow marks
        """
        drag['stack'].group.raise_item()
        pile_image = DragPileItem(self, drag['cards'], func=self.getPosition)
        self.__shade_cache = pile_image.highlight()  # Prevent GC of image
        drag['shadows'].append(ImageItem(self.canvas,
                                         misc.Point(*pile_image.bbox[:2]),
                                         image=self.__shade_cache,
                                         group=self.group))

    def _stopDrag(self):
        """
        _stopDrag() -> None

        Called by cancelDrag() and finishDrag() to clean up after drag pile
        operation.

        :cvar object game.drag: Unset drag pile

        note::
          Subclassed by some Matrix and Mahjongg stacks.
        """
        drag = self.game.drag
        self.__deleteShade()
        drag['canshade_stacks'] = []
        drag['noshade_stacks'] = []
        for s in drag['shadows']:
            s.delete()
        drag['shadows'] = []
        drag['stack'] = None
        drag['cards'] = []

    def finishDrag(self, event=None):
        """
        finishDrag(event:obj=None) -> None

        Finish drag operation

        :param object event: canvas event.
        """
        drag = self.game.drag.copy()
        if CONF_OPT['dragcursor']:
            self.canvas.config(cursor='')
        if CONF_OPT['mouse_type'] == 'point-n-click':
            drag['stack']._stopDrag()
        else:
            self._stopDrag()
        if drag['cards']:
            assert (CONF_OPT['mouse_type'] == 'point-n-click' or
                    drag['stack'] is self)
            self.releaseHandler(event, drag)

    def cancelDrag(self, event=None):
        """
        cancelDrag(event:obj=None) -> None

        Cancel drag operation

        :param object event: canvas event.
        """
        drag = self.game.drag.copy()
        if CONF_OPT['dragcursor']:
            self.canvas.config(cursor='')
        if CONF_OPT['mouse_type'] == 'point-n-click':
            drag['stack']._stopDrag()
        else:
            self._stopDrag()
        if drag['cards']:
            assert drag['stack'] is self
            self.releaseHandler(event, drag)

    def releaseHandler(self, event, drag, sound=True):
        """
        releaseHandler(event:obj, drag:obj[, sound:bool=True]) -> None

        Wrapper for moveCardsBackHandler() to handle differences between mouse
        types upon moving drag['stack']. Perform move with drag['stack'] except
        for *point-n-click* mouse type which uses the pile.

        :param object event: canvas event
        :param object drag: drag pile
        :keyword boolean sound: sound fx?
        """
        if drag['cards']:
            if sound and self.game.can_play_sound:
                self.game._play_sound('nomove')
            if CONF_OPT['mouse_type'] == 'point-n-click':
                Logger.info('void (%s)', drag)
                drag['stack'].moveCardsBackHandler(event, drag)
            else:
                Logger.info('back (%s)', self)
                self.moveCardsBackHandler(event, drag)

    def moveCardsBackHandler(self, event, drag):
        """
        moveCardsBackHandler(event:obj, drag:obj) -> None

        Move drag['stack'] back to original position.

        :param object event: canvas event
        :param object drag: drag pile
        """
        if CONF_OPT['animations']:
            if drag['cards']:
                card = drag['cards'][0]
                xcoord, ycoord = drag['stack'].getPosition(card)
                delta = misc.Delta(x=abs(xcoord - card.x),
                                  y=abs(ycoord - card.y))
                size = self.game.images.size
                Logger.info('delta %s %s', delta, size)
                if delta.x > size.w or delta.x > size.h:
                    from ..animation import animate_play
                    frames = (-1 if delta.x > 2 * size.w or delta.y > 2 * size.h
                                 else 4)
                    Logger.info('frames: %s', frames)
                    animate_play(self.game, from_stack=drag['stack'],
                                 to_stack=drag['stack'], cards=drag['cards'],
                                 x=xcoord, y=ycoord, frames=frames)
        for card in drag['cards']:
            self._position(card)
        if self.is_filled and self.items['shade_item']:
            self.items['shade_item'].show()
            self.items['shade_item'].raise_item()